/** eslint disable */
let devnull = function () { },
    bundleIdCache = {},
    bundleResultCache = {},
    bundleCallbackQueue = {};


/**
 * Subscribe to bundle load event.
 * @param {string[]} bundleIds - Bundle ids
 * @param {Function} callbackFn - The callback function
 */
function subscribe(bundleIds, callbackFn) {
    // listify
    bundleIds = bundleIds.push ? bundleIds : [bundleIds];

    var depsNotFound= [],
        i = bundleIds.length,
        numWaiting = i,
        fn,
        bundleId,
        r,
        q;

    // define callback function
    fn = function (bundleId, pathsNotFound) {
        if (pathsNotFound.length) depsNotFound.push(bundleId);

        numWaiting--;
        if (!numWaiting) callbackFn(depsNotFound);
    };

    // register callback
    while (i--) {
        bundleId = bundleIds[i];

        // execute callback if in result cache
        r = bundleResultCache[bundleId];
        if (r) {
            fn(bundleId, r);
            continue;
        }

        // add to callback queue
        q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];
        q.push(fn);
    }
}


/**
 * Publish bundle load event.
 * @param {string} bundleId - Bundle id
 * @param {string[]} pathsNotFound - List of files not found
 */
function publish(bundleId, pathsNotFound) {
    // exit if id isn't defined
    if (!bundleId) return;

    var q = bundleCallbackQueue[bundleId];

    // cache result
    bundleResultCache[bundleId] = pathsNotFound;

    // exit if queue is empty
    if (!q) return;

    // empty callback queue
    while (q.length) {
        q[0](bundleId, pathsNotFound);
        q.splice(0, 1);
    }
}


/**
 * Execute callbacks.
 * @param {Object or Function} args - The callback args
 * @param {string[]} depsNotFound - List of dependencies not found
 */
function executeCallbacks(args, depsNotFound) {
    // accept function as argument
    if (args.call) args = { success: args };

    // success and error callbacks
    if (depsNotFound.length) (args.error || devnull)(depsNotFound);
    else (args.success || devnull)(args);
}


/**
 * Load individual file.
 * @param {string} path - The file path
 * @param {Function} callbackFn - The callback function
 */
function loadFile(path, callbackFn, args, numTries = null) {
    var doc = document,
        async = args.async,
        maxTries = (args.numRetries || 0) + 1,
        beforeCallbackFn = args.before || devnull,
        pathname = path.replace(/[\?|#].*$/, ''),
        pathStripped = path.replace(/^(css|img)!/, ''),
        isLegacyIECss,
        // @ts-ignore
        e;

    numTries = numTries || 0;

    if (/(^css!|\.css$)/.test(pathname)) {
        // css
        e = doc.createElement('link');
        e.rel = 'stylesheet';
        e.href = pathStripped;

        // tag IE9+
        isLegacyIECss = 'hideFocus' in e;

        // use preload in IE Edge (to detect load errors)
        if (isLegacyIECss && e.relList) {
            isLegacyIECss = 0;
            e.rel = 'preload';
            e.as = 'style';
        }
    } else if (/(^img!|\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {
        // image
        e = doc.createElement('img');
        e.src = pathStripped;
    } else {
        // javascript
        e = doc.createElement('script');
        e.src = path;
        e.async = async === undefined ? true : async;
    }

    e.onload = e.onerror = e.onbeforeload = function (ev) {
        var result = ev.type[0];

        // treat empty stylesheets as failures to get around lack of onerror
        // support in IE9-11
        if (isLegacyIECss) {
            try {
                if (!e.sheet.cssText.length) result = 'e';
            } catch (x) {
                // sheets objects created from load errors don't allow access to
                // `cssText` (unless error is Code:18 SecurityError)
                if (x.code != 18) result = 'e';
            }
        }

        // handle retries in case of load failure
        if (result == 'e') {
            // increment counter
            numTries += 1;

            // exit function and try again
            if (numTries < maxTries) {
                return loadFile(path, callbackFn, args, numTries);
            }
        } else if (e.rel == 'preload' && e.as == 'style') {
            // activate preloaded stylesheets
            return e.rel = 'stylesheet'; // jshint ignore:line
        }

        // execute callback
        callbackFn(path, result, ev.defaultPrevented);
    };

    // add to document (unless callback returns `false`)
    if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);
}


/**
 * Load multiple files.
 * @param {string[]} paths - The file paths
 * @param {Function} callbackFn - The callback function
 */
function loadFiles(paths, callbackFn, args) {
    // listify paths
    paths = paths.push ? paths : [paths];

    var numWaiting = paths.length,
        x = numWaiting,
        pathsNotFound= [],
        fn,
        i;

    // define callback function
    fn = function (path, result, defaultPrevented) {
        // handle error
        if (result == 'e') pathsNotFound.push(path);

        // handle beforeload event. If defaultPrevented then that means the load
        // will be blocked (ex. Ghostery/ABP on Safari)
        if (result == 'b') {
            if (defaultPrevented) pathsNotFound.push(path);
            else return;
        }

        numWaiting--;
        if (!numWaiting) callbackFn(pathsNotFound);
    };

    // load scripts
    for (i = 0; i < x; i++) loadFile(paths[i], fn, args);
}


/**
 * Initiate script load and register bundle.
 * @param {(string|string[])} paths - The file paths
 * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success
 *   callback or (3) object literal with success/error arguments, numRetries,
 *   etc.
 * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object
 *   literal with success/error arguments, numRetries, etc.
 */
function loadjs(paths, arg1, arg2 = null) {
    var bundleId,
        args;

    // bundleId (if string)
    if (arg1 && arg1.trim) bundleId = arg1;

    // args (default is {})
    args = (bundleId ? arg2 : arg1) || {};

    // throw error if bundle is already defined
    if (bundleId) {
        if (bundleId in bundleIdCache) {
            throw "LoadJS";
        } else {
            bundleIdCache[bundleId] = true;
        }
    }

    function loadFn(resolve = null, reject = null) {
        loadFiles(paths, function (pathsNotFound) {
            // execute callbacks
            executeCallbacks(args, pathsNotFound);

            // resolve Promise
            if (resolve) {
                executeCallbacks({ success: resolve, error: reject }, pathsNotFound);
            }

            // publish bundle load event
            publish(bundleId, pathsNotFound);
        }, args);
    }

    if (args.returnPromise) return new Promise(loadFn);
    else loadFn();
}


/**
 * Execute callbacks when dependencies have been satisfied.
 * @param {(string|string[])} deps - List of bundle ids
 * @param {Object} args - success/error arguments
 */
loadjs.ready = function ready(deps, args) {
    // subscribe to bundle load event
    subscribe(deps, function (depsNotFound) {
        // execute callbacks
        executeCallbacks(args, depsNotFound);
    });

    return loadjs;
};


/**
 * Manually satisfy bundle dependencies.
 * @param {string} bundleId - The bundle id
 */
loadjs.done = function done(bundleId) {
    publish(bundleId, []);
};


/**
 * Reset loadjs dependencies statuses
 */
loadjs.reset = function reset() {
    bundleIdCache = {};
    bundleResultCache = {};
    bundleCallbackQueue = {};
};


/**
 * Determine if bundle has already been defined
 * @param String} bundleId - The bundle id
 */
loadjs.isDefined = function isDefined(bundleId) {
    return bundleId in bundleIdCache;
};


// export
export default loadjs;